package com.keyholesoftware.trango.apt;

import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic.Kind;
import javax.tools.FileObject;
import javax.tools.StandardLocation;

import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;
import org.apache.velocity.runtime.log.SystemLogChute;

public abstract class TrangoAnnotationProcessor extends AbstractProcessor {

	private static final String ANNOTATION_ELEMENT = "annotationElement";
	static final String ASPECT_EXTENSION = "Aspect.aj";
	private static final String CLASS_ELEMENT = "classElement";
	protected static final String CONTROLLER_ANNOTATION = "TrangoController";
	protected static final String DATA_HARNESS_ANNOTATION = "TrangoDataHarness";
	protected static final String ENTITY_ANNOTATION = "TrangoEntity";
	protected static final String ENTITY_TEST_ANNOTATION = "TrangoEntityTest";
	protected static final String SEARCH_CRITERIA_ANNOTATION = "TrangoSearchCriteria";
	protected static final String SERVICE_ANNOTATION = "TrangoService";
	protected static final String TEMPLATE_DIRECTORY_OPTION = "templateDirectory";

	protected ProcessingEnvironment _processingEnvironment;
	protected String templateDirectory;

	protected Map<String, Object> buildContext(RoundEnvironment roundEnvironment, TypeElement annotationElement, TypeElement classElement) {
		Map<String, Object> context = new HashMap<String, Object>();
		context.put(CLASS_ELEMENT, classElement);
		context.put(ANNOTATION_ELEMENT, annotationElement);
		return context;
	}

	protected void error(String message) {
		getMessager().printMessage(Kind.ERROR, message);
	}

	protected void flattenAnnotationToContext(String annotationSimpleName, TypeElement classElement, TypeElement annotationElement, Map<String, Object> context) {
		context.put(annotationSimpleName, classElement);
		AnnotationMirror entityAnnotationMirror = getAnnotationMirror(annotationSimpleName, classElement);
		for (Element element : annotationElement.getEnclosedElements()) {
			ExecutableElement executableElement = (ExecutableElement) element;
			note("Annotation Element = " + element);
			note("Annotation Mirror Values =" + entityAnnotationMirror.getElementValues());
			Map<? extends ExecutableElement, ? extends AnnotationValue> actualValues = entityAnnotationMirror.getElementValues();
			AnnotationValue value = actualValues.containsKey(executableElement) ? actualValues.get(executableElement) : executableElement.getDefaultValue();
			context.put(executableElement.getSimpleName().toString(), value);
		}
	}

	protected void generateSource(RoundEnvironment roundEnvironment, TypeElement annotationElement, TypeElement classElement) throws Exception {
		note("GENERATING ASPECTS ....");
		Filer filer = _processingEnvironment.getFiler();
		PackageElement packageElement = (PackageElement) classElement.getEnclosingElement();
		FileObject aspectFileObject = filer.createResource(StandardLocation.SOURCE_OUTPUT, packageElement.getQualifiedName(), getFileName(classElement), classElement);
		Writer writer = aspectFileObject.openWriter();
		note("template directory=" + templateDirectory);
		note("Template = " + getTemplateName());
		Template template = Velocity.getTemplate(getTemplateName());
		VelocityContext velocityContext = new VelocityContext(buildContext(roundEnvironment, annotationElement, classElement));
		template.merge(velocityContext, writer);
		writer.close();
	}

	protected AnnotationMirror getAnnotationMirror(String annotationSimpleName, TypeElement typeElement) {
		AnnotationMirror result = null;
		for (AnnotationMirror mirror : typeElement.getAnnotationMirrors()) {
			if (mirror.getAnnotationType().asElement().getSimpleName().contentEquals(annotationSimpleName)) {
				result = mirror;
				break;
			}
		}
		return result;
	}

	protected String getFileName(TypeElement classElement) {
		return classElement.getSimpleName() + ASPECT_EXTENSION;
	}

	protected Messager getMessager() {
		return _processingEnvironment.getMessager();
	}

	protected abstract String getTemplateName();

	protected TypeElement getTypeElement(RoundEnvironment roundEnvironment, String className) {
		TypeElement result = null;
		for (Element element : roundEnvironment.getRootElements()) {
			if (element.getKind().isClass()) {
				TypeElement currentElement = (TypeElement) element;
				if (currentElement.getQualifiedName().contentEquals(className)) {
					result = (TypeElement) element;
					break;
				}
			}
		}
		return result;
	}

	protected void handleException(Exception e) {
		StringWriter writer = new StringWriter();
		e.printStackTrace(new PrintWriter(writer));
		note(writer.toString());
		error("Exception: " + e.getClass() + " Message: " + e.getMessage());
	}

	@Override
	public synchronized void init(ProcessingEnvironment processingEnvironment) {
		super.init(processingEnvironment);
		_processingEnvironment = processingEnvironment;
		initializeTemplateDirectory();
		initializeVelocity();
	}

	protected void initializeTemplateDirectory() {
		for (String optionKey : _processingEnvironment.getOptions().keySet()) {
			note("Option found: " + optionKey);
			if (optionKey.contains(TEMPLATE_DIRECTORY_OPTION)) {
				templateDirectory = _processingEnvironment.getOptions().get(optionKey);
				break;
			}
		}
		if (templateDirectory == null) {
			try {
				Filer filer = _processingEnvironment.getFiler();
				FileObject tempFileObject = filer.getResource(StandardLocation.CLASS_PATH, "templates", "TrangoController.vm");
				String tempFilename = tempFileObject.toUri().getPath();
				templateDirectory = tempFilename.substring(0, tempFilename.indexOf("TrangoController.vm"));
			} catch (IOException ioe) {
				error(ioe.getMessage());
			}
		}
	}

	protected void initializeVelocity() {
		Properties velocityProperties = new Properties();
		velocityProperties.put(Velocity.RESOURCE_LOADER, "file");
		velocityProperties.put(Velocity.FILE_RESOURCE_LOADER_PATH, templateDirectory);
		velocityProperties.put(Velocity.FILE_RESOURCE_LOADER_CACHE, "false");
		velocityProperties.put(Velocity.VM_LIBRARY, "macros.vm");
		velocityProperties.put(Velocity.VM_LIBRARY_AUTORELOAD, "true");
		velocityProperties.put(Velocity.RUNTIME_LOG_LOGSYSTEM_CLASS, SystemLogChute.class.getName());
		try {
			Velocity.init(velocityProperties);
		} catch (Exception e) {
			handleException(e);
		}
	}

	protected void note(String message) {
		getMessager().printMessage(Kind.NOTE, message);
	}

	@Override
	public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnvironment) {
		System.out.println("Processing Annotations ....");
		for (TypeElement annotationElement : annotations) {
			System.out.println("Processing Annotation = " + annotationElement);
			for (Element element : roundEnvironment.getElementsAnnotatedWith(annotationElement)) {
				TypeElement typeElement = (TypeElement) element;
				try {
					generateSource(roundEnvironment, annotationElement, typeElement);
				} catch (Exception e) {
					handleException(e);
				}
			}
		}
		return true;
	}
}
